package cn.enn.smart.provider;

import cn.enn.smart.check.DefaultPostAuthenticationChecks;
import cn.enn.smart.check.DefaultPreAuthenticationChecks;
import cn.enn.smart.exception.ValidateCodeException;
import cn.enn.smart.service.UserDetailsServiceExtension;
import cn.enn.smart.service.ValidateCodeService;
import cn.enn.smart.token.MobileAuthenticationToken;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.servlet.http.HttpServletRequest;

public class MobileAuthenticationProvider implements AuthenticationProvider {

	private static final Logger logger = LoggerFactory.getLogger(MobileAuthenticationProvider.class);

	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	private final UserCache userCache = new NullUserCache();

	private UserDetailsServiceExtension userDetailsService;

	private static final Pattern MOBILE_PATTERN = Pattern.compile("\\d{11}");

	protected boolean hideUserNotFoundExceptions = true;

	private final UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();

	private final UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks();

	private PasswordEncoder passwordEncoder;

	private boolean forcePrincipalAsString = false;

	private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

	private ValidateCodeService validateCodeService;

	public MobileAuthenticationProvider(UserDetailsServiceExtension userDetailsService,
			PasswordEncoder passwordEncoder) {
		this.userDetailsService = userDetailsService;
		this.passwordEncoder = passwordEncoder;
	}

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		MobileAuthenticationToken mobileAuthenticationToken = (MobileAuthenticationToken) authentication;
		// Determine mobile
		String mobile = (StringUtils.isBlank(mobileAuthenticationToken.getName())) ? "NONE_PROVIDED"
				: authentication.getName();
		boolean cacheWasUsed = true;
		UserDetails user = this.userCache.getUserFromCache(mobile);

		if (user == null) {
			cacheWasUsed = false;

			try {
				user = retrieveUser(mobile, mobileAuthenticationToken);
			} catch (UsernameNotFoundException notFound) {
				logger.debug("Mobile '" + mobile + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException("MobileAuthenticationProvider:" + "用户名或密码错误");
				} else {
					throw notFound;
				}
			}

		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, mobileAuthenticationToken);
		} catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				cacheWasUsed = false;
				user = retrieveUser(mobile, mobileAuthenticationToken);
				preAuthenticationChecks.check(user);
				additionalAuthenticationChecks(user, mobileAuthenticationToken);
			} else {
				throw exception;
			}
		}

		postAuthenticationChecks.check(user);

		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}

		Object principalToReturn = user;

		if (forcePrincipalAsString) {
			principalToReturn = user.getUsername();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return MobileAuthenticationToken.class.isAssignableFrom(authentication);
	}

	protected final UserDetails retrieveUser(String mobile, MobileAuthenticationToken authenticationToken) throws AuthenticationException {
		try {
			UserDetails loadedUser = null;
			Matcher matcher = MOBILE_PATTERN.matcher(mobile);

			if (!authenticationToken.isPasswordLogin()) {
				String validateCode = validateCodeService.loadValidateByUniqueKey(mobile);
				if (!StringUtils.equalsIgnoreCase(validateCode.trim(), authenticationToken.getCredentials().toString().trim())) {
					throw new ValidateCodeException("ValidateCode is invalid");
				}
			}
			if (matcher.matches()) {
				loadedUser = this.getUserDetailsService().loadUserByMobile(mobile);
			} else {
				this.getUserDetailsService().loadUserByUsername(mobile);
			}

			if (loadedUser == null) {
				throw new UsernameNotFoundException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		} catch (UsernameNotFoundException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

	protected void additionalAuthenticationChecks(UserDetails userDetails, MobileAuthenticationToken authentication)
			throws AuthenticationException {

		// 手机号密码模式才校验，如果是手机号验证码模式在此处已经校验过验证码了
		if (authentication.isPasswordLogin()) {
			if (authentication.getCredentials() == null) {
				logger.debug("Authentication failed: no credentials provided");

				throw new BadCredentialsException(messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}

			String presentedPassword = authentication.getCredentials().toString();

			if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
				logger.debug("Authentication failed: password does not match stored value");

				throw new BadCredentialsException(messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
		}
	}

	protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
			UserDetails user) {
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details
		MobileAuthenticationToken result = new MobileAuthenticationToken(principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());

		return result;
	}

	public UserDetailsServiceExtension getUserDetailsService() {
		return userDetailsService;
	}

	public void setUserDetailsService(UserDetailsServiceExtension userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	public boolean isForcePrincipalAsString() {
		return forcePrincipalAsString;
	}

	public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
		this.forcePrincipalAsString = forcePrincipalAsString;
	}

	public ValidateCodeService getValidateCodeService() {
		return validateCodeService;
	}

	public void setValidateCodeService(ValidateCodeService validateCodeService) {
		this.validateCodeService = validateCodeService;
	}
}
